function animation(obj) {
	if (this == window) {
		return new animation(obj);
	} else {
		this.obj = obj;
		this._reset_state();
		this.queue = [];
	}
}
animation.resolution = 20;
animation.offset = 0;
animation.prototype._reset_state = function() {
	this.state = {
		attrs : {},
		duration :500
	}
}
animation.prototype.stop = function() {
	this._reset_state();
	this.queue = [];
	return this;
}
animation.prototype._build_container = function() {
	if (this.container_div) {
		this._refresh_container();
		return;
	}
	var container = document.createElement('div');
	container.style.padding = '0px';
	container.style.margin = '0px';
	container.style.border = '0px';
	var children = this.obj.childNodes;
	while (children.length) {
		container.appendChild(children[0]);
	}
	this.obj.appendChild(container);
	this.obj.style.overflow = 'hidden';
	this.container_div = container;
	this._refresh_container();
}
animation.prototype._refresh_container = function() {
	this.container_div.style.height = 'auto';
	this.container_div.style.width = 'auto';
	this.container_div.style.height = this.container_div.offsetHeight + 'px';
	this.container_div.style.width = this.container_div.offsetWidth + 'px';
}
animation.prototype._destroy_container = function() {
	if (!this.container_div) {
		return;
	}
	var children = this.container_div.childNodes;
	while (children.length) {
		this.obj.appendChild(children[0]);
	}
	this.obj.removeChild(this.container_div);
	this.container_div = null;
}
animation.ATTR_TO = 1;
animation.ATTR_BY = 2;
animation.ATTR_FROM = 3;
animation.prototype._attr = function(attr, value, mode) {
	var auto = false;
	switch (attr) {
	case 'background':
		this._attr('backgroundColor', value, mode);
		return this;
	case 'margin':
		value = animation.parse_group(value);
		this.attr('marginBottom', value[0], mode);
		this.attr('marginLeft', value[1], mode);
		this.attr('marginRight', value[2], mode);
		this.attr('marginTop', value[3], mode);
		return this;
	case 'padding':
		value = animation.parse_group(value);
		this.attr('paddingBottom', value[0], mode);
		this.attr('paddingLeft', value[1], mode);
		this.attr('paddingRight', value[2], mode);
		this.attr('paddingTop', value[3], mode);
		return this;
	case 'backgroundColor':
	case 'borderColor':
	case 'color':
		value = animation.parse_color(value);
		break;
	case 'opacity':
		value = parseFloat(value, 10);
		break;
	case 'height':
	case 'width':
		if (value == 'auto') {
			auto = true;
		} else {
			value = parseInt(value);
		}
		break;
	case 'borderWidth':
	case 'lineHeight':
	case 'fontSize':
	case 'marginBottom':
	case 'marginLeft':
	case 'marginRight':
	case 'marginTop':
	case 'paddingBottom':
	case 'paddingLeft':
	case 'paddingRight':
	case 'paddingTop':
	case 'bottom':
	case 'left':
	case 'right':
	case 'top':
	case 'scrollTop':
	case 'scrollLeft':
		value = parseInt(value, 10);
		break;
	default:
		throw a + ' is not a supported attribute!';
		break;
	}
	if (this.state.attrs[attr] === undefined) {
		this.state.attrs[attr] = {};
	}
	if (auto) {
		this.state.attrs[attr].auto = true;
	}
	switch (mode) {
	case animation.ATTR_FROM:
		this.state.attrs[attr].start = value;
		break;
	case animation.ATTR_BY:
		this.state.attrs[attr].by = true;
	case animation.ATTR_TO:
		this.state.attrs[attr].value = value;
		break;
	}
}
animation.prototype.to = function(attr, value) {
	this._attr(attr, value, animation.ATTR_TO);
	return this;
}
animation.prototype.by = function(attr, value) {
	this._attr(attr, value, animation.ATTR_BY);
	return this;
}
animation.prototype.from = function(attr, value) {
	this._attr(attr, value, animation.ATTR_FROM);
	return this;
}
animation.prototype.duration = function(duration) {
	this.state.duration = duration ? duration : 0;
	return this;
}
animation.prototype.checkpoint = function(distance, callback) {
	if (distance === undefined) {
		distance = 1;
	}
	this.state.checkpoint = distance;
	this.state.checkpointcb = callback;
	this.queue.push(this.state);
	this._reset_state();
	return this;
}
animation.prototype.blind = function() {
	this.state.blind = true;
	return this;
}
animation.prototype.hide = function() {
	this.state.hide = true;
	return this;
}
animation.prototype.show = function() {
	this.state.show = true;
	return this;
}
animation.prototype.ease = function(ease) {
	this.state.ease = ease;
	return this;
}
animation.prototype.go = function() {
	var time = (new Date()).getTime();
	this.queue.push(this.state);
	for ( var i = 0; i < this.queue.length; i++) {
		this.queue[i].start = time - animation.offset;
		if (this.queue[i].checkpoint) {
			time += this.queue[i].checkpoint * this.queue[i].duration;
		}
	}
	animation.push(this);
	return this;
}
animation.prototype._frame = function(time) {
	var done = true;
	var still_needs_container = false;
	var blake_ross = false;
	for ( var i = 0; i < this.queue.length; i++) {
		var cur = this.queue[i];
		if (cur.start > time) {
			done = false;
			continue;
		} else if (cur.checkpointcb
				&& (cur.checkpoint * cur.duration + cur.start > time)) {
			this._callback(cur.checkpointcb, time - cur.start - cur.checkpoint
					* cur.duration);
			cur.checkpointcb = null;
		}
		if (cur.started === undefined) {
			if (cur.show) {
				this.obj.style.display = 'block';
			}
			for ( var a in cur.attrs) {
				if (cur.attrs[a].start !== undefined) {
					continue;
				}
				switch (a) {
				case 'backgroundColor':
				case 'borderColor':
				case 'color':
					var val = animation.parse_color(get_style(this.obj,
							a == 'borderColor' ? 'borderLeftColor' : a));
					if (cur.attrs[a].by) {
						cur.attrs[a].value[0] = Math.min(255, Math.max(0,
								cur.attrs[a].value[0] + val[0]));
						cur.attrs[a].value[1] = Math.min(255, Math.max(0,
								cur.attrs[a].value[1] + val[1]));
						cur.attrs[a].value[2] = Math.min(255, Math.max(0,
								cur.attrs[a].value[2] + val[2]));
					}
					break;
				case 'opacity':
					var val = get_opacity(this.obj);
					if (cur.attrs[a].by) {
						cur.attrs[a].value = Math.min(1, Math.max(0,
								cur.attrs[a].value + val));
					}
					break;
				case 'height':
				case 'width':
					var val = animation['get_' + a](this.obj);
					if (cur.attrs[a].by) {
						cur.attrs[a].value += val;
					}
					break;
				case 'scrollLeft':
				case 'scrollTop':
					var val = (this.obj == document.body ? (document.documentElement || document.body)
							: this.obj)[a];
					if (cur.attrs[a].by) {
						cur.attrs[a].value += val;
					}
					cur['last' + a] = val;
					break;
				default:
					var val = parseInt(get_style(this.obj, a), 10);
					if (cur.attrs[a].by) {
						cur.attrs[a].value += val;
					}
					break;
				}
				cur.attrs[a].start = val;
			}
			if ((cur.attrs.height && cur.attrs.height.auto)
					|| (cur.attrs.width && cur.attrs.width.auto)) {
				if (ua.firefox() < 3) {
					blake_ross = true;
				}
				this._destroy_container();
				for ( var a in {
					height :1,
					width :1,
					fontSize :1,
					borderLeftWidth :1,
					borderRightWidth :1,
					borderTopWidth :1,
					borderBottomWidth :1,
					paddingLeft :1,
					paddingRight :1,
					paddingTop :1,
					paddingBottom :1
				}) {
					if (cur.attrs[a]) {
						this.obj.style[a] = cur.attrs[a].value
								+ (typeof cur.attrs[a].value == 'number' ? 'px'
										: '');
					}
				}
				if (cur.attrs.height && cur.attrs.height.auto) {
					cur.attrs.height.value = animation.get_height(this.obj);
				}
				if (cur.attrs.width && cur.attrs.width.auto) {
					cur.attrs.width.value = animation.get_width(this.obj);
				}
			}
			cur.started = true;
			if (cur.blind) {
				this._build_container();
			}
		}
		var p = (time - cur.start) / cur.duration;
		if (p >= 1) {
			p = 1;
			if (cur.hide) {
				this.obj.style.display = 'none';
			}
		} else {
			done = false;
		}
		if (cur.ease) {
			p = cur.ease(p);
		}
		if (!still_needs_container && p != 1 && cur.blind) {
			still_needs_container = true;
		}
		if (blake_ross && this.obj.parentNode) {
			var parentNode = this.obj.parentNode;
			var nextChild = this.obj.nextSibling;
			parentNode.removeChild(this.obj);
		}
		for ( var a in cur.attrs) {
			switch (a) {
			case 'backgroundColor':
			case 'borderColor':
			case 'color':
				this.obj.style[a] = 'rgb('
						+ animation.calc_tween(p, cur.attrs[a].start[0],
								cur.attrs[a].value[0], true)
						+ ','
						+ animation.calc_tween(p, cur.attrs[a].start[1],
								cur.attrs[a].value[1], true)
						+ ','
						+ animation.calc_tween(p, cur.attrs[a].start[2],
								cur.attrs[a].value[2], true) + ')';
				break;
			case 'opacity':
				set_opacity(this.obj, animation.calc_tween(p,
						cur.attrs[a].start, cur.attrs[a].value));
				break;
			case 'height':
			case 'width':
				this.obj.style[a] = p == 1 && cur.attrs[a].auto ? 'auto'
						: animation.calc_tween(p, cur.attrs[a].start,
								cur.attrs[a].value, true) + 'px';
				break;
			case 'scrollLeft':
			case 'scrollTop':
				var val = this.obj == document.body ? document.documentElement[a]
						|| document.body[a]
						: this.obj[a];
				if (cur['last' + a] != val) {
					delete cur.attrs[a];
				} else {
					var diff = animation.calc_tween(p, cur.attrs[a].start,
							cur.attrs[a].value, true)
							- val;
					if (a == 'scrollLeft') {
						window.scrollBy(diff, 0);
					} else {
						window.scrollBy(0, diff);
					}
					cur['last' + a] = diff + val;
				}
				break;
			default:
				this.obj.style[a] = animation.calc_tween(p, cur.attrs[a].start,
						cur.attrs[a].value, true) + 'px';
				break;
			}
		}
		if (p == 1) {
			this.queue.splice(i--, 1);
			this._callback(cur.ondone, time - cur.start - cur.duration);
		}
	}
	if (blake_ross) {
		parentNode[nextChild ? 'insertBefore' : 'appendChild'](this.obj,
				nextChild);
	}
	if (!still_needs_container && this.container_div) {
		this._destroy_container();
	}
	return !done;
}
animation.prototype.ondone = function(fn) {
	this.state.ondone = fn;
	return this;
}
animation.prototype._callback = function(callback, offset) {
	if (callback) {
		animation.offset = offset;
		callback.call(this);
		animation.offset = 0;
	}
}
animation.calc_tween = function(p, v1, v2, whole) {
	return (whole ? parseInt : parseFloat)((v2 - v1) * p + v1, 10);
}
animation.parse_color = function(color) {
	var hex = /^#([a-f0-9]{1,2})([a-f0-9]{1,2})([a-f0-9]{1,2})$/i.exec(color);
	if (hex) {
		return [ parseInt(hex[1].length == 1 ? hex[1] + hex[1] : hex[1], 16),
				parseInt(hex[2].length == 1 ? hex[2] + hex[2] : hex[2], 16),
				parseInt(hex[3].length == 1 ? hex[3] + hex[3] : hex[3], 16) ];
	} else {
		var rgb = /^rgba? *\(([0-9]+), *([0-9]+), *([0-9]+)(?:, *([0-9]+))?\)$/
				.exec(color);
		if (rgb) {
			if (rgb[4] === '0') {
				return [ 255, 255, 255 ];
			} else {
				return [ parseInt(rgb[1], 10), parseInt(rgb[2], 10),
						parseInt(rgb[3], 10) ];
			}
		} else if (color == 'transparent') {
			return [ 255, 255, 255 ];
		} else {
			throw 'Named color attributes are not supported.';
		}
	}
}
animation.parse_group = function(value) {
	var value = trim(value).split(/ +/);
	if (value.length == 4) {
		return value;
	} else if (value.length == 3) {
		return [ value[0], value[1], value[2], value[1] ];
	} else if (value.length == 2) {
		return [ value[0], value[1], value[0], value[1] ];
	} else {
		return [ value[0], value[0], value[0], value[0] ];
	}
}
animation.get_height = function(obj) {
	var pT = parseInt(get_style(obj, 'paddingTop'), 10), pB = parseInt(
			get_style(obj, 'paddingBottom'), 10), bT = parseInt(get_style(obj,
			'borderTopWidth'), 10), bW = parseInt(get_style(obj,
			'borderBottomWidth'), 10);
	return obj.offsetHeight - (pT ? pT : 0) - (pB ? pB : 0) - (bT ? bT : 0)
			- (bW ? bW : 0);
}
animation.get_width = function(obj) {
	var pL = parseInt(get_style(obj, 'paddingLeft'), 10), pR = parseInt(
			get_style(obj, 'paddingRight'), 10), bL = parseInt(get_style(obj,
			'borderLeftWidth'), 10), bR = parseInt(get_style(obj,
			'borderRightWidth'), 10);
	return obj.offsetWidth - (pL ? pL : 0) - (pR ? pR : 0) - (bL ? bL : 0)
			- (bR ? bR : 0);
}
animation.push = function(instance) {
	if (!animation.active) {
		animation.active = [];
	}
	animation.active.push(instance);
	if (!animation.timeout) {
		animation.timeout = setInterval(animation.animate.bind(animation),
				animation.resolution);
	}
}
animation.animate = function() {
	var done = true;
	var time = (new Date()).getTime();
	for ( var i = 0; i < animation.active.length; i++) {
		if (animation.active[i]._frame(time)) {
			done = false;
		} else {
			animation.active.splice(i--, 1);
		}
	}
	if (done) {
		clearInterval(animation.timeout);
		animation.timeout = null;
	}
}
animation.ease = {}
animation.ease.begin = function(p) {
	return p * p;
}
animation.ease.end = function(p) {
	p -= 1;
	return -(p * p) + 1;
}
animation.ease.both = function(p) {
	if (p <= 0.5) {
		return (p * p) * 2;
	} else {
		p -= 1;
		return (p * p) * -2 + 1;
	}
}